/*	--- 线程同步 --- 


当多个线程同时运行时，线程的调度由操作系统决定，程序本身无法决定。

因此，任何一个线程都有可能在任何指令处被操作系统暂停，然后在某个时间段后继续执行。

-----------------

这个时候，有个单线程模型下不存在的问题就来了：如果多个线程同时读写共享变量，会出现数据不一致的问题。

我们来看一个例子：  */

// 多线程
public class Main {
	public static void main(String[] args) throws Exception {
		var add = new AddThread();
		var dec = new DecThread();

		add.start();
		dec.start();

		add.join();
		dec.join();

		System.out.println(Counter.counter);
	}
}

class Counter {
	public static int count = 0;
}

class AddThread extends Thread {
	public void run() {
		for (int i=0; i<10000; i++) {
			Counter.count += 1;
		}
	}
}

class DecThread extends Thread {
	public void run() {
		for (int i=0; i<10000; i++) {
			Counter.count -= 1;
		}
	}
}


/*
上面的代码很简单，两个线程同时对一个int变量进行操作，一个加10000次，一个减10000次，最后结果应该是0，但是，每次运行，结果实际上都是不一样的。

这是因为对变量进行读取和写入时，结果要正确，必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。

例如，对于语句：
	n = n + 1;

看上去是一行语句，实际上对应了3条指令：

	I LOAD
	I ADD
	I STORE


--------------------------------------

我们假设n的值是100，如果两个线程同时执行n = n + 1，得到的结果很可能不是102，而是101，原因在于：

	┌───────┐     ┌───────┐
	│Thread1│     │Thread2│
	└───┬───┘     └───┬───┘
	    │             │
	    │ILOAD (100)  │
	    │             │ILOAD (100)
	    │             │IADD
	    │             │ISTORE (101)
	    │IADD         │
	    │ISTORE (101) │
	    ▼             ▼


如果'线程1'在执行ILOAD后被操作系统中断;

此刻, 如果'线程2'被调度执行，它执行ILOAD后获取的值仍然是100，

最终结果被两个线程的ISTORE都写入后变成了101，而不是期待的102。


----------------------------------------------------


这说明多线程模型下，要保证逻辑正确，对【共享变量】进行读写时，必须保证一组指令以原子方式执行：即某一个线程执行时，其他线程必须等待：

	┌───────┐     	┌───────┐
	│Thread1│     	│Thread2│
	└───┬───┘     	└───┬───┘
	    │             	│
	    │-- lock --   	│
	    │ILOAD (100)  	│
	    │IADD         	│
	    │ISTORE (101) 	│
	    │-- unlock -- 	│
	    │             	│-- lock --
	    │             	│ILOAD (101)
	    │             	│IADD
	    │             	│ISTORE (102)
	    │             	│-- unlock --
	    ▼               ▼

通过加锁(lock)和解锁(unlock)的操作，就能保证3条指令总是在一个线程执行期间，不会有其他线程会进入此指令区间。

即使在执行期线程被操作系统中断执行，其他线程也会因为无法获得'锁'导致无法进入此指令区间。

只有【执行线程】将'锁'释放后，其他线程才有机会获得锁并执行。

这种'加锁'和'解锁'之间的代码块我们称之为临界区(Critical Section)，任何时候'临界区'最多【只有一个线程】能执行。

可见，保证一段代码的原子性就是通过'加锁'和'解锁'实现的。

Java程序使用synchronized关键字对一个'对象'进行加锁： */
synchronized(lock) {
	n = n + 1;
}

// synchronized保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized改写如下： 

// 多线程
public class Main {
	public static void main(String[] args) throws Exception {
		var add = new AddThread();
		var dec = new DecThread();

		add.start();
		dec.start();

		add.join();
		dec.join();

		System.out.println(Counter.count);
	}
}

class Counter {
	public static final Object lock = new Object();
	public static int count = 0;
}

class AddThread extends Thread {
	public void run() {
		for (int i=0; i<10000; i++) {
			synchronized(Counter.lock) {
				Counter.count += 1;
			}
		}
	}
}

class DecThread extends Thread {
	public void run() {
		for (int i=0; i<10000; i++) {
			synchronized(Counter.lock) {
				Counter.count -= 1;
			}
		}
	}
}


// 注意到代码：
synchronized(Counter.lock) { // 获取锁
	// ...
} // 释放锁


/*
它表示用Counter.lock实例作为锁，两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时，必须先获得锁，才能进入代码块进行。

执行结束后，在synchronized语句块结束会自动释放锁。

这样一来，对Counter.count变量进行读写时,就不可能同时被多个线程进行rw。

上述代码无论运行多少次，最终结果都是0。


------------------------------

使用synchronized解决了‘多线程’同步访问【共享变量】的正确性问题。

但是，它的缺点是带来了性能下降。

因为synchronized代码块无法并发执行。

此外，‘加锁’和‘解锁’需要消耗一定的时间，所以，synchronized会降低程序的执行效率。

-------------------------------------

我们来概括一下如何使用synchronized：

	1. 找出修改共享变量的线程代码块；
	2. 选择一个共享实例作为‘锁’；
	3. 使用synchronized(lockObject) { ... }。


在使用synchronized的时候，不必担心抛出异常。因为无论是否有异常，都会在synchronized结束处正确释放锁： */
public void add(int m) {
	synchronized(object) {
		if (m < 0) {
			throw new RuntimeException();
		}

		this.value += m;
	} // 无论是否有异常, 都会在此处释放lock
}


// 我们再来看一个错误使用synchronized的例子：
public class Main {
	public static void main(String[] args) throws Exception {
		var add = new AddThread();
		var dec = new DecThread();

		add.start();
		dec.start();

		add.join();
		dec.join();

		System.out.println(Counter.count);
	}
}

class Counter {
	public static final Object lock1 = new Object();
	public static final Object lock2 = new Object();

	public static int count = 0;
}

class AddThread extends Thread {
	public void run() {
		for (int i=0; i<10000; i++) {
			synchronized(Counter.lock1) {
				Counter.count += 1;
			}
		}
	}
}

class DecThread extends Thread {
	public void run() {
		for (int i=0; i<10000; i++) {
			synchronized(Counter.lock2) {
				Counter.count -= 1;
			}
		}
	}
}

/*
结果并不是0，这是因为两个线程各自的synchronized锁住的不是同一个对象！

这使得两个线程各自都可以同时获得锁：
	因为JVM只保证同一个锁在任意时刻只能被‘一个线程’所获取，但两个不同的锁在同一时刻可以被两个线程分别获取。


因此，使用synchronized的时候，‘获取到的是哪个锁’非常重要。

‘锁对象’如果不对，代码逻辑就不对。

---------------------------

我们再看一个例子：  */
public class Main {
	public static void main(String[] args) throws Exception {
		var ts = new Thread[] { 
			new AddStudentThread(),
			new AddTeacherThread(),
			new DecTeacherThread()
		};

		for (var t : ts) {
			t.start();
		}

		for (var t : ts) {
			t.join(); // Thread main等各个t结束后,再运行
		}

		System.out.println(Counter.student_Count);
		System.out.println(Counter.teacher_Count);
	}
}

class Counter {
	public static final Object lock = new Object();
	public static int student_Count = 0;
	public static int teacher_Count = 0;
}

class AddStudentThread extends Thread {
	public void run() {
		for (int i=0; i<10000; i++) {
			synchronized(Counter.lock) {
				Counter.student_Count += 1;
			}
		}
	}
}

class DecStudentThread extends Thread {
	public void run() {
		for (int i=0; i<10000; i++) {
			synchronized(Counter.lock) {
				Counter.student_Count -= 1;
			}
		}
	}
}

class AddTeacherThread extends Thread {
	public void run() {
		for (int i=0; i<10000; i++) {
			synchronized(Counter.lock) {
				Counter.teacher_Count += 1;
			}
		}
	}
}

class DecTeacherThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.teacher_Count -= 1;
            }
        }
    }
}


/*
上述代码的'4个线程'对'2个共享变量'分别进行读写操作，但是使用的锁都是Counter.lock这一个对象，这就造成了原本可以并发执行的
	Counter.studentCount += 1和
	Counter.teacherCount += 1,
现在无法并发执行了，执行效率大大降低。


实际上，需要同步的线程可以分成两组：
	1-1 AddStudentThread
	1-2 DecStudentThread，

	2-1 AddTeacherThread
	2-2 DecTeacherThread，

组之间不存在竞争，因此，应该使用两个不同的锁，即：

AddStudentThread 和 DecStudentThread 使用 lock_Student锁： */
synchronized(Counter.lock_Student) {
	//...
}


// AddTeacherThread 和 DecTeacherThread 使用lock_Teacher锁：
synchronized(Counter.lock_Teacher) {
	//...
}

// 这样才能最大化地提高执行效率。



/* --------------------------------------------------


#	不需要synchronized的操作



JVM规范定义了几种原子操作：

	· 基本类型（long和double除外）赋值。 例如：int n = m；
	· 引用类型赋值。 例如：List<String> list = anotherList。


long和double是64位数据，JVM没有明确规定64位赋值操作是不是一个原子操作，不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。

单条原子操作的语句不需要同步。例如： */
public void set(int m) {
	synchronized(lock) {
		this.value = m;
	}
} 
// 就不需要同步。


// 对引用也是类似。例如：
public void set(String s) {
	this.value = s;
} 
// 上述赋值语句并不需要同步。


/*----------------------------------------

但是，如果是多行赋值语句，就必须保证是同步操作，例如： */
class Pair {
	int first;
	int last;

	public void set(int first, int last) {
		synchronized(this) {
			this.first = first;
			this.last = last;
		}
	}
}


// 有些时候，通过一些巧妙的转换，可以把非原子操作变为原子操作。例如，上述代码如果改造成：
class Pair {
	int[] pair;

	public void set(int first, int last) {
		int[] ps = new int[] { first, last };
		this.pair = ps;
	}
}

//就不再需要同步，因为this.pair = ps是引用赋值的原子操作。而语句：
int[] ps = new int[] { first, last };


// 这里的ps是方法内部定义的局部变量，每个线程都会有各自的局部变量，互不影响，并且互不可见，并不需要同步。




/*	---- 线程同步 の 小结 ---- 


1. ‘多线程’同时读写‘共享变量’时，会造成逻辑错误，因此需要通过synchronized上锁共享变量所在的class的Object来同步；

2. 同步的本质就是给‘指定对象’加锁，加锁后才能继续执行后续代码；

3. 注意加锁对象必须是同一个实例；

4. 对JVM定义的单个原子操作不需要同步。


*/